Skip to content

Support integer indexing of Categorical and MultivariateNormal predictions - #442

Open
alejandroschuler wants to merge 6 commits into
masterfrom
fix/distn-int-index
Open

alejandroschuler wants to merge 6 commits into
masterfrom
fix/distn-int-index

Conversation

@alejandroschuler

Copy link
Copy Markdown
Collaborator

Depends on #441. This branch starts from fix/normal-fixed-params, because #441 adds the indexing tests that this PR changes. Until #441 merges, the diff here also shows its commit. Review the last commit only, or wait for #441.

dist[0] (an integer key) raised an error for Bernoulli, k_categorical(k) and MultivariateNormal(k). Distn.__getitem__ passes params[:, i] to the constructor, which is a one-dimensional array for an integer key. The constructors of these classes read params.shape[1]. Slices such as dist[1:4] worked. The other distributions in ngboost.distns accept an integer key.

Design

The fix is in the constructors. dist[i] is now the distribution of row i without the row axis, as for the other distributions and as for a numpy array:

  • Categorical.__init__ builds the logits with shape (K,) + params.shape[1:], and sample1 draws probs.shape[1:] random numbers in place of (1, len(self)).
  • MultivariateNormal and get_chol_factor use ... and the last axes in place of fixed axis numbers in __init__, summaries, logpdf and rv. scipy_distribution() gives one scipy object for one row and a list for a batch.
  • ClassificationDistn.predict takes the argmax over the last axis. For a batch this is the same as axis 1.

I also looked at two other places for the fix. An override of __getitem__ in the three classes, or a change in Distn.__getitem__, would pass params[:, [i]] and keep the row axis. Then dist[i] would be the same as dist[i:i+1], a batch of one row, with class_probs() of shape (1, K) and a MultivariateNormal loc of shape (1, p). For Normal, dist[0].loc is a scalar, so these three classes would differ from the rest. The test from #441 compares dist[i].params with row i of dist.params, and MultivariateNormal would fail it. A change in Distn.__getitem__ would also need a check of the class, so that dist[i] stays the same for the classes that work now. The constructor fix needs no such check, and it does not touch Distn.__getitem__ or the Distn.__getattr__ that #433 adds.

For a batch, the outputs do not change. I compared the old and new code on batches of 7 rows for k_categorical(2), k_categorical(3), MultivariateNormal(2) and MultivariateNormal(3): class_probs, predict, seeded sample, loc, cov, logpdf, summaries, and the LogScore d_score and metric of the manifold are bit-identical. A batch of one row (dist[0:1]) also gives the same logpdf and seeded sample as before.

Example

d = Dist(np.random.default_rng(0).normal(scale=0.3, size=(Dist.n_params, 4))), then d[0]:

Dist #441 this PR
Bernoulli ValueError: not enough values to unpack (expected 2, got 1) class_probs() shape (2,), predict() one class
k_categorical(3) same ValueError class_probs() shape (3,), predict() one class
MultivariateNormal(2) IndexError: tuple index out of range loc (2,), cov (2, 2), logpdf(y) a scalar

Iteration (for row in dist) now works for these classes too, because it calls __getitem__ with 0, 1, 2 and so on.

One difference stays. BetaBernoulli comes from the sympy factory, and its dist[0].class_probs() has shape (1, 2) because its class_probs calls np.column_stack. This PR does not change it.

Tests

🤖 Generated with Claude Code

alejandroschuler and others added 2 commits October 26, 2026 22:17
Distn.__getitem__ and Distn.__len__ read self._params. NormalFixedVar and
NormalFixedMean override __init__ without calling the base constructor, so
_params stayed None. On the output of pred_dist, dist[0:2] raised
"TypeError: 'NoneType' object is not subscriptable" and len(dist) raised an
AttributeError. Both constructors now set self._params, as LogNormal and
Exponential do.

A parametrized test builds every distribution in ngboost.distns.__all__ and
checks len, slicing and integer indexing. Only the two fixed Normal classes
failed on master. Integer indexing is marked xfail for Bernoulli,
k_categorical and MultivariateNormal, which fail for a different reason
(their constructors need two-dimensional params).

Found while assessing #168.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…tions

Distn.__getitem__ passes params[:, i] to the constructor for an integer key,
so the constructor gets a one-dimensional array. The constructors of the
k_categorical classes (Bernoulli included) and of MultivariateNormal read
params.shape[1], so dist[0] raised an error. Slices worked.

The constructors now accept params without the row axis, and the methods that
used fixed axis numbers use the last axes instead. dist[i] is then the
distribution of row i without the row axis, as for the other distributions
and as for a numpy array: class_probs() has shape (K,), predict() gives one
class, MultivariateNormal has loc (p,) and cov (p, p), logpdf(y) gives a
scalar, and scipy_distribution() gives one scipy object. dist[i:i+1] still
gives a batch of one row. ClassificationDistn.predict takes the argmax over
the last axis, which is the same as axis 1 for a batch. For batches the
outputs are unchanged.

The integer-index test no longer marks these three classes as xfail. New
tests check class_probs, predict, sample, logpdf, cov, scipy_distribution and
iteration on dist[i], and dist[0] of fitted NGBClassifier and NGBRegressor
models against predict_proba and predict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alejandroschuler and others added 4 commits November 9, 2026 12:05
Master rewrote RELEASE_NOTES.md into sections for 0.6.0. The conflict is
resolved by taking master's version and adding the #441 and #442 entries
under Bug fixes. Master already lists the CRPScore metric fix (PR #424).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Master added two #434 entries at the end of Bug fixes in RELEASE_NOTES.md,
where this branch adds the #441 and #442 entries. The conflict keeps both,
with master's entries first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Master (#438) and this branch both added tests at the end of
tests/test_distns.py. The conflict keeps both blocks: the indexing tests
of this branch, then the MultivariateNormal params tests of #438.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#438 documented params for a batch of N rows. dist[i] gives loc of shape p
and cov of shape p x p.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant